﻿using CodeBuilderForVS2022.Entity;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using RazorEngine;
using RazorEngine.Configuration;
using RazorEngine.Templating;
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Engine = RazorEngine.Engine;
using Task = System.Threading.Tasks.Task;

namespace CodeBuilderForVS2022
{
    /// <summary>
    /// Command handler
    /// </summary>
    internal sealed class FlyadminCodeBuilderCommand
    {
        public static DTE2 _dte;

        /// <summary>
        /// Command ID.
        /// </summary>
        public const int CommandId = 0x0100;

        /// <summary>
        /// Command menu group (command set GUID).
        /// </summary>
        public static readonly Guid CommandSet = new Guid("9f15e097-b3b1-494e-912e-3b18cbc7af85");

        /// <summary>
        /// VS Package that provides this command, not null.
        /// </summary>
        private readonly AsyncPackage package;

        /// <summary>
        /// Initializes a new instance of the <see cref="FlyadminCodeBuilderCommand"/> class.
        /// Adds our command handlers for menu (commands must exist in the command table file)
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        /// <param name="commandService">Command service to add command to, not null.</param>
        private FlyadminCodeBuilderCommand(AsyncPackage package, OleMenuCommandService commandService)
        {
            InitRazorEngine();
            this.package = package ?? throw new ArgumentNullException(nameof(package));
            commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

            var menuCommandID = new CommandID(CommandSet, CommandId);
            var menuItem = new MenuCommand(this.Execute, menuCommandID);
            commandService.AddCommand(menuItem);
        }

        /// <summary>
        /// 初始化RazorEngine
        /// </summary>
        private void InitRazorEngine()
        {
            var config = new TemplateServiceConfiguration
            {
                TemplateManager = new EmbeddedResourceTemplateManager(typeof(Template.Template))
            };
            Engine.Razor = RazorEngineService.Create(config);
        }


        /// <summary>
        /// Gets the instance of the command.
        /// </summary>
        public static FlyadminCodeBuilderCommand Instance
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the service provider from the owner package.
        /// </summary>
        private Microsoft.VisualStudio.Shell.IAsyncServiceProvider ServiceProvider
        {
            get
            {
                return this.package;
            }
        }

        /// <summary>
        /// Initializes the singleton instance of the command.
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        public static async Task InitializeAsync(AsyncPackage package, DTE2 dte)
        {
            // Switch to the main thread - the call to AddCommand in FlyadminCodeBuilderCommand's constructor requires
            // the UI thread.
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
            _dte = dte;
            OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
            Instance = new FlyadminCodeBuilderCommand(package, commandService);
        }


        /// <summary>
        /// This function is the callback used to execute the command when the menu item is clicked.
        /// See the constructor to see how the menu item is associated with this function using
        /// OleMenuCommandService service and MenuCommand class.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event args.</param>
        private void Execute(object sender, EventArgs e)
        {
            ThreadHelper.ThrowIfNotOnUIThread();

            //加载动画
            LoadingForm loadingForm = new LoadingForm();
            loadingForm.Show();
            try
            {
                if (_dte.SelectedItems.Count == 1)
                {
                    SelectedItem selectedItem = _dte.SelectedItems.Item(1);
                    ProjectItem selectProjectItem = selectedItem.ProjectItem;
                    //获取当前点击的类所在的项目
                    Project topProject = selectProjectItem.ContainingProject;

                    //api目录
                    string apiDir = GetApiDir(topProject);

                    ////当前类在当前项目中的目录结构
                    string dirPath = GetSelectFileDirPath(topProject, selectProjectItem);
                    //当前类命名空间
                    string namespaceStr = selectProjectItem.FileCodeModel.CodeElements.OfType<CodeNamespace>().First().FullName;
                    //当前项目根命名空间
                    string applicationStr = "";
                    if (!string.IsNullOrEmpty(namespaceStr))
                    {
                        applicationStr = namespaceStr.Substring(0, namespaceStr.IndexOf("."));
                    }
                    //当前类
                    CodeClass codeClass = GetClass(selectProjectItem.FileCodeModel.CodeElements);
                    //当前项目类名
                    string className = codeClass.Name;
                    string cnName = codeClass.DocComment.Replace("<doc>", "").Replace("<summary>", "").Replace("</doc>", "").Replace("</summary>", "").Replace("\r\n", "");


                    #region 错误提示
                    if (!ConfigHelper.IsFlyadminApi(apiDir))
                    {
                        loadingForm.ShowError("本代码生成器只适用于FlyAdmin.WebApi");
                        return;
                    }
                    if (!ConfigHelper.IsDataTable(apiDir, namespaceStr))
                    {
                        loadingForm.ShowError("请选择需要生成的数据表实体类");
                        return;
                    }
                    #endregion

                    loadingForm.ShowLoading("正在生成Input", 1);
                    //生成DTO
                    var inputProjectItem = topProject.ProjectItems.Item("Input").ProjectItems.Item(className);
                    if (inputProjectItem == null)
                    {
                        inputProjectItem = topProject.ProjectItems.Item("Input").ProjectItems.AddFolder(className);
                    }
                    DtoFileModel dtoModel = GetDtoModel(applicationStr, className, cnName, cnName, dirPath, codeClass);
                    CreateInputFile(dtoModel, className, inputProjectItem, loadingForm);

                    //loadingForm.ShowLoading("正在生成Output", 2);

                    var outputProjectItem = topProject.ProjectItems.Item("Output").ProjectItems.Item(className);
                    if (outputProjectItem == null)
                    {
                        outputProjectItem = topProject.ProjectItems.Item("Output").ProjectItems.AddFolder(className);
                    }
                    CreateOutputFile(dtoModel, className, outputProjectItem, loadingForm);

                    //loadingForm.ShowLoading("正在生成数据访问层", 3);
                    CreateServiceFile(applicationStr, className, cnName, dirPath, loadingForm, codeClass);


                    loadingForm.ShowLoading($"正在编辑Mapper文件", 10);
                    SetMapper(topProject, applicationStr, className, cnName);
                }
                else
                {
                    loadingForm.ShowError("请选择需要生成的数据表实体类");
                }
            }
            catch (Exception ex)
            {
                loadingForm.ShowError(ex.Message);
            }
        }

        #region 代码生成

        /// <summary>
        /// 获取DtoModel
        /// </summary>
        /// <param name="applicationStr"></param>
        /// <param name="name"></param>
        /// <param name="dirName"></param>
        /// <param name="codeClass"></param>
        /// <returns></returns>
        private DtoFileModel GetDtoModel(string applicationStr, string name, string cnName, string description, string dirName, CodeClass codeClass)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            var model = new DtoFileModel() { Namespace = applicationStr, Name = name, CnName = cnName, Description = description, DirName = dirName.Replace("\\", ".") };
            List<ClassProperty> classProperties = new List<ClassProperty>();

            foreach (CodeElement item in codeClass.Bases)
            {
                classProperties.Add(new ClassProperty()
                {
                    Name = "Id",
                    CnName = "主键",
                    PropertyType = item.Name == "BaseGuidEntity" ? "string" : "int"
                }); ;
            }


            var codeMembers = codeClass.Members;
            foreach (CodeElement codeMember in codeMembers)
            {
                if (codeMember.Kind == vsCMElement.vsCMElementProperty)
                {
                    ClassProperty classProperty = new ClassProperty();
                    CodeProperty property = codeMember as CodeProperty;
                    classProperty.Name = property.Name;
                    //获取属性类型
                    var propertyType = property.Type;
                    switch (propertyType.TypeKind)
                    {
                        case vsCMTypeRef.vsCMTypeRefString:
                            classProperty.PropertyType = "string";
                            break;
                        case vsCMTypeRef.vsCMTypeRefInt:
                            classProperty.PropertyType = "int";
                            break;
                        case vsCMTypeRef.vsCMTypeRefBool:
                            classProperty.PropertyType = "bool";
                            break;
                        case vsCMTypeRef.vsCMTypeRefDecimal:
                            classProperty.PropertyType = "decimal";
                            break;
                        case vsCMTypeRef.vsCMTypeRefDouble:
                            classProperty.PropertyType = "double";
                            break;
                        case vsCMTypeRef.vsCMTypeRefFloat:
                            classProperty.PropertyType = "float";
                            break;
                        case vsCMTypeRef.vsCMTypeRefCodeType:
                            classProperty.PropertyType = propertyType.AsFullName;
                            break;
                    }

                    string propertyCnName = property.DocComment.Replace("<doc>", "").Replace("<summary>", "").Replace("</doc>", "").Replace("</summary>", "").Replace("\r\n", "");//属性中文名称

                    classProperty.CnName = string.IsNullOrEmpty(propertyCnName) ? property.Name : propertyCnName;

                    classProperties.Add(classProperty);
                }
            }

            model.ClassPropertys = classProperties;

            return model;
        }

        #region 生成后端

        /// <summary>
        /// 创建Dto类
        /// </summary>
        /// <param name="model"></param>
        /// <param name="name"></param>
        /// <param name="dtoFolder"></param>
        private void CreateInputFile(DtoFileModel model, string name, ProjectItem dtoFolder, LoadingForm loadingForm)
        {
            loadingForm.ShowLoading($"正在生成$Add{name}Input.cs", 2);
            string addInputContent = Engine.Razor.RunCompile("AddInput", typeof(DtoFileModel), model);
            string fileNameAddInput = $"Add{name}Input.cs";
            AddFileToProjectItem(dtoFolder, addInputContent, fileNameAddInput);

            loadingForm.ShowLoading($"正在生成Update{name}Input.cs", 3);
            string updateInputContent = Engine.Razor.RunCompile("UpdateInput", typeof(DtoFileModel), model);
            string fileNameUpdateInput = $"Update{name}Input.cs";
            AddFileToProjectItem(dtoFolder, updateInputContent, fileNameUpdateInput);

            loadingForm.ShowLoading($"正在生成Get{name}PageInput.cs", 4);
            string getPageInputContent = Engine.Razor.RunCompile("GetPageInput", typeof(DtoFileModel), model);
            string fileNameGetPageInput = $"Get{name}PageInput.cs";
            AddFileToProjectItem(dtoFolder, getPageInputContent, fileNameGetPageInput);
        }

        /// <summary>
        /// 创建Dto类
        /// </summary>
        /// <param name="model"></param>
        /// <param name="name"></param>
        /// <param name="dtoFolder"></param>
        private void CreateOutputFile(DtoFileModel model, string name, ProjectItem dtoFolder, LoadingForm loadingForm)
        {
            loadingForm.ShowLoading($"正在生成Get{name}Output.cs", 5);
            string queryOutputContent = Engine.Razor.RunCompile("GetOutput", typeof(DtoFileModel), model);
            string fileNameQueryOutput = $"Get{name}Output.cs";
            AddFileToProjectItem(dtoFolder, queryOutputContent, fileNameQueryOutput);

            loadingForm.ShowLoading($"正在生成Get{name}PageOutput.cs", 6);
            string getPageOutputContent = Engine.Razor.RunCompile("GetPageOutput", typeof(DtoFileModel), model);
            string fileNameGetPageOutput = $"Get{name}PageOutput.cs";
            AddFileToProjectItem(dtoFolder, getPageOutputContent, fileNameGetPageOutput);
        }

        /// <summary>
        /// 创建Service类
        /// </summary>
        /// <param name="applicationStr">根命名空间</param>
        /// <param name="name">类名</param>
        /// <param name="dtoFolder">父文件夹</param>
        /// <param name="dirName">类所在文件夹目录</param>
        private void CreateServiceFile(string applicationStr, string name, string cnName, string dirName, LoadingForm loadingForm, CodeClass codeClass)
        {
            var model = new DALFileModel() { Namespace = applicationStr, Name = name, CnName = cnName, DirName = dirName.Replace("\\", ".") };
            #region Prop

            List<ClassProperty> classProperties = new List<ClassProperty>();
            var codeMembers = codeClass.Members;
            foreach (CodeElement codeMember in codeMembers)
            {
                if (codeMember.Kind == vsCMElement.vsCMElementProperty)
                {
                    ClassProperty classProperty = new ClassProperty();
                    CodeProperty property = codeMember as CodeProperty;
                    classProperty.Name = property.Name;
                    //获取属性类型
                    var propertyType = property.Type;
                    switch (propertyType.TypeKind)
                    {
                        case vsCMTypeRef.vsCMTypeRefString:
                            classProperty.PropertyType = "string";
                            break;
                        case vsCMTypeRef.vsCMTypeRefInt:
                            classProperty.PropertyType = "int";
                            break;
                        case vsCMTypeRef.vsCMTypeRefBool:
                            classProperty.PropertyType = "bool";
                            break;
                        case vsCMTypeRef.vsCMTypeRefDecimal:
                            classProperty.PropertyType = "decimal";
                            break;
                        case vsCMTypeRef.vsCMTypeRefDouble:
                            classProperty.PropertyType = "double";
                            break;
                        case vsCMTypeRef.vsCMTypeRefFloat:
                            classProperty.PropertyType = "float";
                            break;
                    }

                    string propertyCnName = property.DocComment.Replace("<doc>", "").Replace("<summary>", "").Replace("</doc>", "").Replace("</summary>", "").Replace("\r\n", "");//属性中文名称

                    classProperty.CnName = string.IsNullOrEmpty(propertyCnName) ? property.Name : propertyCnName;

                    classProperties.Add(classProperty);
                }
            }

            model.ClassPropertys = classProperties;

            #endregion

            //数据访问层接口
            var projects = _dte.Solution.Projects.OfType<Project>();
            var projectIDAL = projects.FirstOrDefault(x => x.Name == $"{applicationStr}.IDAL");

            loadingForm.ShowLoading($"正在生成I{name}DAL.cs", 7);
            string content_IDAL = Engine.Razor.RunCompile("IDAL", typeof(DALFileModel), model);
            string fileName_IDAL = $"I{name}DAL.cs";
            AddFileToProjectItem(projectIDAL.ProjectItems, content_IDAL, fileName_IDAL);

            //数据访问层实现
            loadingForm.ShowLoading($"正在生成{name}DAL.cs", 8);
            var projectDAL = projects.FirstOrDefault(x => x.Name == $"{applicationStr}.DAL");
            string content_dal = Engine.Razor.RunCompile("DAL", typeof(DALFileModel), model);
            string fileName_dal = $"{name}DAL.cs";
            AddFileToProjectItem(projectDAL.ProjectItems, content_dal, fileName_dal);

            //控制器api
            loadingForm.ShowLoading($"正在生成{name}ControllerL.cs", 9);
            var projectAPI = projects.FirstOrDefault(x => x.Name == $"{applicationStr}.WebApi");
            string content_api = Engine.Razor.RunCompile("Controller", typeof(DALFileModel), model);
            string fileName_api = $"{name}Controller.cs";
            AddFileToProjectItem(projectAPI.ProjectItems.Item("Controllers"), content_api, fileName_api);
        }



        /// <summary>
        /// 编辑CustomDtoMapper.cs,添加映射
        /// </summary>
        /// <param name="applicationProject"></param>
        /// <param name="className"></param>
        /// <param name="classCnName"></param>
        private void SetMapper(Project applicationProject, string applicationStr, string className, string classCnName)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            ProjectItem customDtoMapperProjectItem = _dte.Solution.FindProjectItem(applicationProject.FileName.Substring(0, applicationProject.FileName.LastIndexOf("\\")) + "\\Base\\MapperProfiles.cs");
            if (customDtoMapperProjectItem != null)
            {
                CodeClass codeClass = GetClass(customDtoMapperProjectItem.FileCodeModel.CodeElements);
                var codeEdit = codeClass.Namespace.StartPoint.CreateEditPoint();
                codeEdit.Insert($"using {applicationStr}.Entity.Input.{className};\r\n");
                codeEdit.Insert($"using {applicationStr}.Entity.Output.{className};\r\n");
                codeEdit.Insert("\r\n");
                var codeChilds = codeClass.Members;
                foreach (CodeElement codeChild in codeChilds)
                {
                    if (codeChild.Kind == vsCMElement.vsCMElementNamespace)
                    {
                        var insertCode = codeChild.GetEndPoint(vsCMPart.vsCMPartAttributes).CreateEditPoint();
                        insertCode.Insert($"using {applicationStr}.Entity.Input.{className};\r\n");
                        insertCode.Insert($"using {applicationStr}.Entity.Output.{className};\r\n");
                    }
                    if (codeChild.Kind == vsCMElement.vsCMElementFunction && codeChild.Name == "MapperProfiles")
                    {
                        var insertCode = codeChild.GetEndPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
                        insertCode.Insert($"            // {classCnName}\r\n");
                        insertCode.Insert($"            CreateMap<{className}, Add{className}Input>().ReverseMap();\r\n");
                        insertCode.Insert($"            CreateMap<{className}, Get{className}Output>().ReverseMap();\r\n");
                        insertCode.Insert($"            CreateMap<{className}, Get{className}PageOutput>().ReverseMap();\r\n");
                        insertCode.Insert($"            CreateMap<{className}, Update{className}Input>().ReverseMap();\r\n");
                        insertCode.Insert("\r\n");
                    }
                }

                customDtoMapperProjectItem.Save();
            }
        }

        #endregion

        #endregion

        #region 辅助方法

        /// <summary>
        /// 获取Api所在的目录
        /// </summary>
        /// <param name="topProject"></param>
        /// <returns></returns>
        private string GetApiDir(Project topProject)
        {
            var arr = topProject.FileName.Split('\\');
            string projFullName = arr[arr.Length - 2];
            string projName = projFullName.Split('.')[1];
            projFullName = projFullName.Replace(projName, "WebApi");
            var newList = arr.ToList();
            newList.RemoveAt(arr.Length - 1);
            newList.RemoveAt(arr.Length - 2);
            newList.Add(projFullName);
            return string.Join("\\", newList);
        }

        /// <summary>
        /// 获取所有项目
        /// </summary>
        /// <param name="projectItems"></param>
        /// <returns></returns>
        private IEnumerable<ProjectItem> GetProjects(ProjectItems projectItems)
        {
            if (projectItems != null)
            {
                foreach (EnvDTE.ProjectItem item in projectItems)
                {
                    yield return item;

                    if (item.SubProject != null)
                    {
                        foreach (EnvDTE.ProjectItem childItem in GetProjects(item.SubProject.ProjectItems))
                            if (childItem.Kind == EnvDTE.Constants.vsProjectItemKindSolutionItems)
                                yield return childItem;
                    }
                    else
                    {
                        foreach (EnvDTE.ProjectItem childItem in GetProjects(item.ProjectItems))
                            if (childItem != null && childItem.Kind == EnvDTE.Constants.vsProjectItemKindSolutionItems)
                                yield return childItem;
                    }
                }
            }

        }

        /// <summary>
        /// 获取类
        /// </summary>
        /// <param name="codeElements"></param>
        /// <returns></returns>
        private CodeClass GetClass(CodeElements codeElements)
        {
            var elements = codeElements.Cast<CodeElement>().ToList();
            var result = elements.FirstOrDefault(codeElement => codeElement.Kind == vsCMElement.vsCMElementClass) as CodeClass;
            if (result != null)
            {
                return result;
            }
            foreach (var codeElement in elements)
            {
                result = GetClass(codeElement.Children);
                if (result != null)
                {
                    return result;
                }
            }
            return null;
        }

        /// <summary>
        /// 获取当前所选文件去除项目目录后的文件夹结构
        /// </summary>
        /// <param name="selectProjectItem"></param>
        /// <returns></returns>
        private string GetSelectFileDirPath(Project topProject, ProjectItem selectProjectItem)
        {
            string dirPath = "";
            if (selectProjectItem != null)
            {
                //所选文件对应的路径
                string fileNames = selectProjectItem.FileNames[0];
                string selectedFullName = fileNames.Substring(0, fileNames.LastIndexOf('\\'));

                //所选文件所在的项目
                if (topProject != null)
                {
                    //项目目录
                    string projectFullName = topProject.FullName.Substring(0, topProject.FullName.LastIndexOf('\\'));

                    //当前所选文件去除项目目录后的文件夹结构
                    dirPath = selectedFullName.Replace(projectFullName, "");
                }
            }

            return dirPath.Substring(1);
        }

        /// <summary>
        /// 添加文件到项目中
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="content"></param>
        /// <param name="fileName"></param>
        private void AddFileToProjectItem(ProjectItem folder, string content, string fileName)
        {
            string path = Path.GetTempPath();
            Directory.CreateDirectory(path);
            string file = Path.Combine(path, fileName);
            File.WriteAllText(file, content, System.Text.Encoding.UTF8);
            try
            {
                folder.ProjectItems.AddFromFileCopy(file);
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// 添加文件到项目中
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="content"></param>
        /// <param name="fileName"></param>
        private void AddFileToProjectItem(ProjectItems folder, string content, string fileName)
        {
            string path = Path.GetTempPath();
            Directory.CreateDirectory(path);
            string file = Path.Combine(path, fileName);
            File.WriteAllText(file, content, System.Text.Encoding.UTF8);
            try
            {
                folder.AddFromFileCopy(file);
            }
            finally
            {
                File.Delete(file);
            }
        }
        #endregion
    }
}
